#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>

#include "../include/transportADT.h"

//estructura de los mensajes que se guardaran en la memoria compartida.
typedef struct {
	int transId;		//id del proceso para utilizar la memoria.
	char msg[MSG_LEN];	//mensaje que se compartira en la comunicacion.
} shMsgBuf;

struct transportCDT{
	int shmReadId;			//id de la memoria compartida de lectura
	shMsgBuf * shReadBuf;	//buffer de lectura
	int shmWriteId;			//id de la memoria compartida de escritura
	shMsgBuf * shWriteBuf;	//buffer de escritura
	int orId;		//id del proceso que esta usando el transporte
	int destId;		//id del proceso al que se desea enviar el mensaje
	char isOpen;	//flag que indica si el transporte se llamo con open o get
	int semId1;		//	id de los semaforos encargados de sincronizar
	int semId2;		//	el envio y la recepcion de mensajes
	struct sembuf p[4];	//estructuras para el uso de semaforos
	struct sembuf v[4];
};

//union necesaria para setear semaforos
typedef union{
	int              val;    /* Value for SETVAL */
	struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
	unsigned short  *array;  /* Array for GETALL, SETALL */
	struct seminfo  *__buf;  /* Buffer for IPC_INFO */
}semun;

static struct transportCDT * getShMem(void * id, int flags);
static int getSem(int id, struct transportCDT * trans, int flags);
static void fillSemBuf(struct sembuf * s_buf, int num, int op, int flag);

transportADT
openTrans(void * id){
	struct transportCDT * trans;

	//trato de reservar una nueva porcion de memoria compartida
	if ((trans = getShMem(id, IPC_CREAT | IPC_EXCL)) == NULL) {
		return NULL;
	}
	trans->orId = *((int *)id);	//guardo mi id

	//trato de abrir los semaforos que se usaran para sincronizar
	if (getSem(*((int *)id), trans, IPC_CREAT | IPC_EXCL)< 0) {
		closeTrans(trans);
	}

	return trans;
}

transportADT
getTrans(void * id, void * dest){
	struct transportCDT * trans;

	//trato de usar la posicion de memoria ya reservada por dest
	if ((trans = getShMem(dest, IPC_CREAT)) == NULL) {
		return NULL;
	}
	trans->destId = *((int *)dest);
	trans->orId = *((int *)id);

	//trato de usar los semaforos ya abiertos por dest
	if (getSem(*((int *)dest), trans, IPC_CREAT) < 0) {
		closeTrans(trans);
		return NULL;
	}

	return trans;
}

//devuelve la estructura de transporte con la porcion de memoria compartida
//ya atacheada, y su id guardado en la estructura
static struct transportCDT *
getShMem(void * id, int flags) {
	struct transportCDT * trans;

	//uso el flag isOpen para asignar los id de read y write de forma acorde,
	//dependiendo de si se llamo al ADT con open o con get
	char isOpen = (flags == IPC_CREAT)? 0:1;

	if((trans=malloc(sizeof(struct transportCDT)))==NULL)
		exit(1);

	//voy a reservar 2 porciones de memoria para lectura y escritura con el id
	//pasado por parametro, y el id + 1. dependiendo de si se llego a esta
	//funcion a traves de openTrans o getTrans, voy a asignar Read y Write de
	//manera distinta (para que el write de uno vaya al read de otro
	//(y viceversa). Reservo el tamaño de mi mensaje definido arriba.
	if((trans->shmReadId = shmget(*((int *)id)+(!isOpen), sizeof(shMsgBuf),
			flags | 0666)) == -1){
		fprintf(stderr, "No se pudo iniciar el transporte\n");
		return NULL;
	}

	if((trans->shmWriteId = shmget(*((int *)id)+isOpen, sizeof(shMsgBuf),
			flags | 0666)) == -1){
		fprintf(stderr, "No se pudo iniciar el transporte\n");
		shmctl(trans->shmReadId, IPC_RMID, NULL);
		return NULL;
	}

	//hago "attach" de las posiciones de memoria reservadas
	//para indicar que los voy a utilizar
	if((trans->shReadBuf = shmat(trans->shmReadId, NULL, 0)) == ((void*)-1)){
		fprintf(stderr, "No se pudo reservar la memoria de lectura\n");
		closeTrans(trans);
		return NULL;
	}

	if((trans->shWriteBuf = shmat(trans->shmWriteId, NULL, 0)) == ((void*)-1)){
		fprintf(stderr, "No se pudo reservar la memoria de escritura\n");
		closeTrans(trans);
		return NULL;
	}

	//guardo el flag de isOpen para futuro uso
	trans->isOpen = isOpen;

	return trans;
}

//guarda en trans el id de los semaforos ya inicializados
static int
getSem(int id, struct transportCDT * trans, int flags) {
	semun x;

	//abro los semaforos encargados de sincronizar la comunicacion
	if ((trans->semId1 = semget(id, 4, flags | 0666)) == -1) {
		fprintf(stderr, "Error con la sincronizacion de la memoria\n");
		return -1;
	}

	if ((trans->semId2 = semget(id+1, 3, flags | 0666)) == -1) {
		fprintf(stderr, "Error con la sincronizacion de la memoria\n");
		return -1;
	}

	//si soy el que creo el transporte, seteo los valores iniciales de los semaforos
	if (trans->isOpen) {
		x.val = 1;
		if (semctl(trans->semId1, 0, SETVAL, x) == -1 ||
				semctl(trans->semId2, 0, SETVAL, x) == -1 ||
				semctl(trans->semId1, 3, SETVAL, x) == -1) {
			fprintf(stderr, "Error con la sincronizacion de la memoria\n");
			return -1;
		}

		x.val = 0;
		if (semctl(trans->semId1, 1, SETVAL, x) == -1 ||
				semctl(trans->semId2, 1, SETVAL, x) == -1 ||
				semctl(trans->semId1, 2, SETVAL, x) == -1 ||
				semctl(trans->semId2, 2, SETVAL, x) == -1) {
			fprintf(stderr, "Error con la sincronizacion de la memoria\n");
			return -1;
		}

	}

	//lleno los buffer de semaforos con las operaciones que realizare sobre
	// los mismos
	int i;
	for(i = 0; i < 4; i++)
		fillSemBuf(&(trans->p[i]), i, -1, 0);

	for(i = 0; i < 4; i++)
		fillSemBuf(&(trans->v[i]), i, 1, 0);

	return 1;
}

//setea una estructura sembuf con los valores pasados por parametro
static void
fillSemBuf(struct sembuf * s_buf, int num, int op, int flag) {
	s_buf->sem_num = num;
	s_buf->sem_op = op;
	s_buf->sem_flg = flag;
}

int
readMsg(transportADT trans, void * buf, int size){

	//dependiendo de quien esta llamando writeMsg, decido que hacer
	if (trans->isOpen == 1) {

		//si puedo consumir 1 unidad, quiere decir que puedo leer
		semop(trans->semId1, &(trans->p[1]), 1);

		//guardo el destino del que envia para poder responderle
		trans->destId = trans->shReadBuf->transId;
		//guardo el mensaje
		memcpy(buf, trans->shReadBuf->msg, size);

		//aumento en 1 unidad para destrabar al proceso de escritura actual
		semop(trans->semId1, &(trans->v[2]), 1);
		//aumento en 1 unidad para que el proximo proceso de escritura
		//que quiera escribir pueda hacerlo
		semop(trans->semId1, &(trans->v[0]), 1);
	}
	else {
		semop(trans->semId2, &(trans->p[1]), 1);

		//como soy el que hizo get del transporte, me fijo si el mensaje
		//esta dirigido para mi
		if (trans->orId == trans->shReadBuf->transId) {
			memcpy(buf, trans->shReadBuf->msg, size);
		}
		else {
			//si no estaba dirigido a mi, devuelvo -1
			size = -1;
		}

		semop(trans->semId2, &(trans->v[2]), 1);
		semop(trans->semId2, &(trans->v[0]), 1);
	}

	return size;
}

int
writeMsg(transportADT trans, const void * buf, int size){
	if (size > MSG_LEN)
		return -1;

	//dependiendo de quien esta llamando writeMsg, decido que hacer
	if (trans->isOpen == 1) {
		semop(trans->semId2, &(trans->p[0]), 1);
		trans->shWriteBuf->transId = trans->destId;
		memcpy(trans->shWriteBuf->msg, buf, size);

		semop(trans->semId2, &(trans->v[1]), 1);
		semop(trans->semId2, &(trans->p[2]), 1);
	}
	else {
		//como soy el que hizo get, voy a tener que sincronizarme
		//con otros procesos, asi que primero trato de consumir 1 unidad
		semop(trans->semId1, &(trans->p[3]), 1);

		//consumo una unidad para ver si puedo empezar a escribir
		semop(trans->semId1, &(trans->p[0]), 1);

		//seteo como id del mensaje mi id, para que me puedan responder
		trans->shWriteBuf->transId = trans->orId;
		memcpy(trans->shWriteBuf->msg, buf, size);

		//aviso al proceso de lectura que puede leer, aumentando 1 unidad
		semop(trans->semId1, &(trans->v[1]), 1);

		//el proceso de lectura me va a bloquear hasta que termine de leer,
		//para que no me adelante mucho y genere problemas con los otros
		//procesos que hicieron el get
		semop(trans->semId1, &(trans->p[2]), 1);

		//aumento 1 unidad sobre lo que consumi al principio, para que otros
		//procesos puedan hacer write
		semop(trans->semId1, &(trans->v[3]), 1);
	}

	return size;
}

void
closeTrans(transportADT trans){
	if (trans == NULL)
		return;

	//cierro las dos porciones de memoria compartida y los dos semaforos
	shmctl(trans->shmReadId, IPC_RMID, NULL);
	shmctl(trans->shmWriteId, IPC_RMID, NULL);
	semctl(trans->semId1, IPC_RMID, 0);
	semctl(trans->semId2, IPC_RMID, 0);

	free(trans);
}
